今天我們將繼續了解 pattern matching 的語法。
昨天只講到 pattern 如果是「特定值」那我們做什麼行為,但如果我們是有多個參數要一起參與判斷式的組成,那感覺就不太夠用了。
所以 Haskell 為我們提供了 Guard ,我們先來寫了一個 function 來判斷三個邊長是否可以構成一個合法的三角型
calcTriangle :: Int -> Int -> Int -> String
calcTriangle a b c
| a + b <= c || a + c <= b || b + c <= a = "Invalid"
| a == b && b == c = "Equilateral"
| a == b || b == c || a == c = "Isosceles"
| otherwise = "Scalene"
Guard 的用法很簡單,只要用 |
並在後面加上一個最後會回傳 Bool
的 expression 以及match到後要回傳的 expression ,然後依序從上到下如果失敗就會往下繼續 match 。
首先我們用三角形任意兩邊一定大於第三邊來當作判斷,如果成立為 True
就會回傳 "Invalid"
,如果不是就會接到下一個 guard ,接下來我們來判斷如果三邊相等那就代表他為正三角形所以回傳 "Equilateral"
,如果還不是就會繼續到下一個 guard ,以此類推直到最後的 otherwise
。
otherwise
就是 True
,意思就是一定會執行右邊的 expression ,通常就是放在最後面有點類似其他語言的 switch case
的 default
之類的用途。但如果沒有提供 otherwise
且最後沒有匹配到任何一個 guard ,那跟一般的 pattern matching 一樣最後會拋出錯誤。
上面的程式感覺還是有點羅嗦,有什麼辦法可以簡化重複的 pattern 嗎?這時候我們可以使用 where
幫定義我們需要重複的 expression。
calcTriangle' :: Int -> Int -> Int -> String
calcTriangle' a b c
| a + b <= c || a + c <= b || b + c <= a = "Invalid"
| abEq && bcEq = "Equilateral"
| abEq || bcEq || acEq = "Isosceles"
| otherwise = "Scalene"
where
abEq = a == b
bcEq = b == c
acEq = a == c
where
的用法就是放在 |
的後面然後定義名稱及值 ,所以我們可以 a == b
等等的布林運算定義成另外一個名稱,然後原本的 | a == b && b == c = "Equilateral"
就可以變成 | abEq && bcEq = "Equilateral"
讓我們在使用 pattern matching 時增加整體的可讀性,且 where
中所綁定的名稱只在這個 function 也就是 calcTriangle'
裡才有用。
除了綁定數值以外,where
也可以用來綁定 function
calcTriangle'' :: Int -> Int -> Int -> String
calcTriangle'' a b c
| isValid a b c || isValid a c b || isValid b c a= "Invalid"
| abEq && bcEq = "Equilateral"
| abEq || bcEq || acEq = "Isosceles"
| otherwise = "Scalene"
where
abEq = a == b
bcEq = b == c
acEq = a == c
isValid x y z = x + y <= z
我們可以把 a + b <= c
這類的運算改用 where
來定義成一個 fucntion ,雖然在這裡看不出有太多的差異或者有什麼理由一定要這樣寫,但假設我們今天有一個很小的 function 想要抽出原本的 expression 但又不需要寫成全域的 function 我們也可以用 where
來協助我們達成這件事情。
拿我們前幾天的 List comprehension 的例子舉例:
[email | email <- emailList, elem '@' email && elem '.' email ]
原本我們有一個 List comprehension 然後他的限制條件是判斷 email
裡有無 @
且 .
但假設我們想把這個判斷抽出來也可以使用 where
來達成
filterValidEmail :: [String] -> [String]
filterValidEmail xs =
[email | email <- xs, isValid email ]
where
isValid email = '@' `elem` email && '.' `elem` email
這裡可以看到我們使用 where
來將 isValid
的邏輯抽出來,然後我再把原本的限制條件改為使用 isValid
總結來說 where
有點像是局部變數的概念,我們可以將某些數值或者 function 固定在某個 scope 有作用而已。
雖然沒有特別提到,但在 Haskell 中 function 也是一種變數。
說到區域變數,也許有人會疑惑那跟 let
又差在哪裡?
我們先把上面的程式碼改為用 let in
filterValidEmail' :: [String] -> [String]
filterValidEmail' xs =
let
isValid email = '@' `elem` email && '.' `elem` email
in
[email | email <- xs, isValid email]
跟之前所介紹的一樣 let
綁定的名稱只在 in
裡面有作用,let
與 where
的根本差異是 in
後面是接一個 expression 且 let
可以在任何地方使用
-- ghci
foo = let a = 200 in a +1
foo -- 201
foo = [let a = 200 in a+1 , 202]
foo -- [201,202]
那至於該選擇 let
還是 where
呢?只能說看習慣與場合,但多數情況**「我個人認為」** where
比較好讀一點點
case
使用其實就真的很像其他語言的 switch case
一樣 ,我們 case
一個值然後根據他的值而執行什麼。
sumList :: Num a => [a] -> a
sumList [] = 0
sumList (x:xs) = x + sumList xs
sumList' :: Num a => [a] -> a
sumList' list = case list of
[] -> 0
(x:xs) -> x + sumList' xs
那這樣 sumList
跟 sumList'
有什麼差?答案就是「沒差」,我們在 function 使用的 pattern matching 就只是 case
的語法糖而已。
但 case
看的出來也是一種 expression 所以我們一樣能在任何地方使用
foo x =
[case x of
'a' -> 10
'b' -> 11
'c' -> 12
'd' -> 13
'e' -> 14
'f' -> 15
_ -> error "Invalid hex digit"
,1]
今天大致上講到了大部分 pattern matching 的用法及特性了,相信各位應該對於 Haskell 的認識又更深了一層。
想起之前與同事有討論過 pattern matching (沒有特指哪一個的語言實作)跟 js/ts 的 switch case 差在哪裡,最直觀的差異是一個通常是 expression 而一個是 statement ,這個差異就就讓我們在使用場景差很多了,
因為只要是 expression 我可以大部分地方使用並取得我們要的值,但如果是 statement 我們可能需要用一個 function 包裝起來最後回傳一個值或者是一個 mutable variable 在各個 case 中間去更新值。
以及 pattern matching 不只是匹配 「值」 而已,而是連 「結構」 都可以匹配,以及匹配對象(或者該說流程的分支)的彈性都強大許多。也不難想像會什麼會有ts-pattern這個 library 的出現,畢竟 pattern matching 的好,用過就知道。
今天的程式碼:https://github.com/toddLiao469469/30days-for-haskell